import bpy
import bmesh
from bpy.types import Operator
from mathutils import Vector, Matrix, noise, Quaternion


def bounds(curr_mesh):
    """
    Finds the upper and lower bound of a BMesh. Returns a tuple containing the
    lower bound as the first element; the upper bound as the second.
    """
    lb = Vector((100000.0, 100000.0, 100000.0))
    ub = Vector((-100000.0, -100000.0, -100000.0))
    verts = curr_mesh.verts
    for vert in verts:
        co = vert.co

        if co.x < lb.x:
            lb.x = co.x
        if co.y < lb.y:
            lb.y = co.y
        if co.z < lb.z:
            lb.z = co.z

        if co.x > ub.x:
            ub.x = co.x
        if co.y > ub.y:
            ub.y = co.y
        if co.z > ub.z:
            ub.z = co.z

    return (lb, ub)


def vec_mix(a, b, t):
    """
    Mixes between two vectors, an origin and a destination, by a factor in the
    range [0.0, 1.0] .
    """
    u = 1.0 - t
    return Vector((
        u * a.x + t * b.x,
        u * a.y + t * b.y,
        u * a.z + t * b.z))


def simplecut():
    D = bpy.data
    C = bpy.context

    bm = bmesh.new()
    bmesh.ops.create_cube(bm, size=0.5)

    # Create a plane with a random normal
    # at a random coordinate within half the cube's size.
    plane_co = 0.25 * noise.random_vector(size=3)
    plane_no = noise.random_unit_vector(size=3)

    # Copy the source cube.
    bm_slice_0 = bm.copy()

    # Concatenate the mesh's vertices, edges and faces
    # into a single list.
    geom_0 = list(bm_slice_0.verts) + \
        list(bm_slice_0.edges) + \
        list(bm_slice_0.faces)

    # Bisect the mesh.
    result_0 = bmesh.ops.bisect_plane(
        bm_slice_0,
        geom=geom_0,
        plane_co=plane_co,
        plane_no=plane_no,
        clear_inner=False,
        clear_outer=True)

    # bisect_plane returns a dictionary.
    # Acquire the geometry created along the 'cut'.
    geom_cut_0 = result_0["geom_cut"]

    # Retrieve only the edges from the list of geometry.
    new_edges = []
    for el in geom_cut_0:
        if isinstance(el, bmesh.types.BMEdge):
            new_edges.append(el)

    # The cut is open, so bridge it with a face.
    bmesh.ops.edgenet_fill(bm_slice_0, edges=new_edges)

    # Convert the slice to an object.
    mesh_data = D.meshes.new("Cube.Slice.000")
    bm_slice_0.to_mesh(mesh_data)
    mesh_obj = D.objects.new(mesh_data.name, mesh_data)
    C.collection.objects.link(mesh_obj)
    bm_slice_0.free()

    # Repeat the process above, but with the opposite Boolean
    # values for clear_inner and clear_outer parameters.
    bm_slice_1 = bm.copy()
    geom_1 = list(bm_slice_1.verts) + \
        list(bm_slice_1.edges) + \
        list(bm_slice_1.faces)

    result_1 = bmesh.ops.bisect_plane(
        bm_slice_1,
        geom=geom_1,
        plane_co=plane_co,
        plane_no=plane_no,
        clear_inner=True,
        clear_outer=False)

    geom_cut_1 = result_1["geom_cut"]
    new_edges = []
    for el in geom_cut_1:
        if isinstance(el, bmesh.types.BMEdge):
            new_edges.append(el)

    bmesh.ops.edgenet_fill(bm_slice_1, edges=new_edges)

    mesh_data = D.meshes.new("Cube.Slice.001")
    bm_slice_1.to_mesh(mesh_data)
    mesh_obj = D.objects.new(mesh_data.name, mesh_data)
    C.collection.objects.link(mesh_obj)
    bm_slice_1.free()

def complexcut():
    D = bpy.data
    C = bpy.context
    # Create a Suzanne face
    # facing the opposite direction on the y axis.
    bm = bmesh.new()
    scl = Matrix.Diagonal((0.5, -0.5, 0.5, 1.0))
    identity = Matrix.Identity(4)
    bmesh.ops.create_monkey(bm, matrix=scl, calc_uvs=True)

    # Recalculate face normals due to flipped y axis.
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    # Subdivide the mesh.
    # Smooth falloff options: ['SMOOTH', 'SPHERE', 'ROOT',
    #   'SHARP', 'LINEAR', 'INVERSE_SQUARE']
    # Quad corner type options: ['STRAIGHT_CUT',
    #   'INNER_VERT', 'PATH', 'FAN']
    bmesh.ops.subdivide_edges(
        bm,
        edges=bm.edges,
        cuts=3,
        smooth=0.75,
        smooth_falloff='SMOOTH',
        quad_corner_type='STRAIGHT_CUT',
        use_grid_fill=True,
        use_only_quads=False,
        use_smooth_even=False)

    # Find lower bound, upper bound and center point.
    lb, ub = bounds(bm)
    center = (ub + lb) * 0.5

    segments = 32
    seg_range = range(0, segments)
    to_percent = 1.0 / (segments - 1.0)

    # Find origin, destination coordinate of plane.
    origin = Vector((lb.x, center.y, center.z))
    dest = Vector((ub.x, center.y, center.z))

    # Find normal of plane.
    plane_no = dest - origin
    plane_no.normalize()

    # Cache newly created mesh objects in a list.
    mesh_objs = []
    for segment in seg_range:

        # Opening plane slice.
        percent0 = (segment - 0.5) * to_percent
        plane_co_0 = vec_mix(origin, dest, percent0)

        # Closing plane slice.
        percent1 = (segment + 0.5) * to_percent
        plane_co_1 = vec_mix(origin, dest, percent1)

        # Copy source BMesh to new BMesh.
        bm_slice = bm.copy()

        # Concatenate BMesh data.
        geom = bm_slice.verts[:] + bm_slice.edges[:] + bm_slice.faces[:]

        result = bmesh.ops.bisect_plane(
            bm_slice,
            geom=geom,
            plane_co=plane_co_0,
            plane_no=plane_no,
            clear_inner=True,
            clear_outer=False)

        geom = bm_slice.verts[:] + bm_slice.edges[:] + bm_slice.faces[:]

        result = bmesh.ops.bisect_plane(
            bm_slice,
            geom=geom,
            plane_co=plane_co_1,
            plane_no=plane_no,
            clear_inner=False,
            clear_outer=True)

        # Guard against creation of empty meshes.
        if len(bm_slice.verts) > 0:

            # Find center of individual slice.
            lb, ub = bounds(bm_slice)
            center = (ub + lb) * 0.5

            # Shift mesh data by pivot.
            bmesh.ops.translate(
                bm_slice,
                vec=-center,
                space=identity,
                verts=bm_slice.verts)

            # Solidify.
            geom = bm_slice.verts[:] + \
                bm_slice.edges[:] + bm_slice.faces[:]

            bmesh.ops.solidify(
                bm_slice,
                geom=geom,
                thickness=0.0025)

            mesh_name = "Slice.{0:03}".format(segment)
            mesh_data = D.meshes.new(mesh_name)
            bm_slice.to_mesh(mesh_data)
            bm_slice.free()

            # Link object.
            mesh_obj = D.objects.new(mesh_data.name, mesh_data)
            C.collection.objects.link(mesh_obj)

            # Transfer mesh center to object transform location.
            mesh_obj.location = center
            mesh_objs.append(mesh_obj)

            # Free original Suzanne BMesh.
    bm.free()

class Draw_Bisection_Op(Operator):
    bl_idname = "object.draw_bisection"
    bl_label = "draw bisection"
    bl_description = "draw a bisection"


    def execute(self, context):
        simplecut()
        # complexcut()
        return {'FINISHED'}
